using System.Collections.Generic;
using System.Text;
using NFreeMarker.Template;

namespace NFreeMarker.Core
{
    /**
     * An element representing a macro declaration.
     */

    public sealed class Macro : TemplateElement, ITemplateModel
    {
        private readonly string _name;
        private readonly string[] _argumentNames;
        private readonly Dictionary<string, Expression> _args;
        private string _catchAll;

        internal static readonly Macro DoNothingMacro = new Macro(
            ".pass", 
            new List<string>(0),
            new Dictionary<string, Expression>(0),
            TextBlock.EmptyBlock);

        internal Macro(
            string name,
            List<string> argumentNames,
            Dictionary<string, Expression> args,
            TemplateElement nestedBlock)
        {
            _name = name;
            _argumentNames = argumentNames.ToArray();
            _args = args;
            NestedBlock = nestedBlock;
        }

        public string GetCatchAll()
        {
            return _catchAll;
        }

        public void SetCatchAll(string value)
        {
            _catchAll = value;
        }

        public string[] GetArgumentNames()
        {
            return (string[]) _argumentNames.Clone();
        }

        internal string[] GetArgumentNamesInternal()
        {
            return _argumentNames;
        }

        internal bool HasArgNamed(string name)
        {
            return _args.ContainsKey(name);
        }

        public string GetName()
        {
            return _name;
        }

        internal override void Accept(Environment env)
        {
            env.VisitMacroDef(this);
        }

        public override string GetCanonicalForm()
        {
            string directiveName = IsFunction ? "function" : "macro";
            var buf = new StringBuilder("<#");
            buf.Append(directiveName);
            buf.Append(' ');
            buf.Append(_name);
            buf.Append('(');
            int size = _argumentNames.Length;
            for (int i = 0; i < size; i++)
            {
                buf.Append(_argumentNames[i]);
                if (i != (size - 1))
                {
                    buf.Append(",");
                }
            }
            buf.Append(")>");
            if (NestedBlock != null)
            {
                buf.Append(NestedBlock.GetCanonicalForm());
            }
            buf.Append("</#");
            buf.Append(directiveName);
            buf.Append('>');
            return buf.ToString();
        }

        public override string GetDescription()
        {
            return (IsFunction ? "function " : "macro ") + _name;
        }

        public bool IsFunction
        {
            get;
            internal set;
        }

        internal class Context : ILocalContext
        {
            private readonly Environment.Namespace _localVars;
            private readonly Macro _macro;
            internal readonly TemplateElement Body;
            internal readonly Environment.Namespace BodyNamespace;
            internal readonly List<string> BodyParameterNames;
            internal readonly Context PrevMacroContext;
            internal readonly List<ILocalContext> PrevLocalContextStack;

            internal Context(
                Macro macro,
                Environment env,
                TemplateElement body,
                List<string> bodyParameterNames) 
            {
                _localVars = new Environment.Namespace(env);
                PrevMacroContext = env.GetCurrentMacroContext();
                BodyNamespace = env.GetCurrentNamespace();
                PrevLocalContextStack = env.GetLocalContextStack();
                _macro = macro;
                Body = body;
                BodyParameterNames = bodyParameterNames;
            }

            internal Macro GetMacro()
            {
                return _macro;
            }

            internal void RunMacro(Environment env)
            {
                SanityCheck(env);
                // Set default values for unspecified parameters
                if (_macro.NestedBlock != null)
                {
                    env.Visit(_macro.NestedBlock);
                }
            }

            // Set default parameters, check if all the required parameters are defined.
            private void SanityCheck(Environment env)
            {
                bool resolvedAnArg, hasUnresolvedArg;
                Expression firstUnresolvedExpression;
                InvalidReferenceException firstReferenceException;
                do
                {
                    firstUnresolvedExpression = null;
                    firstReferenceException = null;
                    resolvedAnArg = hasUnresolvedArg = false;
                    foreach (string argName in _macro._argumentNames)
                    {
                        if (_localVars.Get(argName) != null) continue;

                        Expression valueExp = _macro._args[argName];
                        if (valueExp == null)
                        {
                            throw new TemplateException(
                                "Error executing macro: " + _macro._name + "\nrequired parameter: " + argName +
                                " is not specified.", env);
                        }

                        try
                        {
                            ITemplateModel tm = valueExp.GetAsTemplateModel(env);
                            if (tm == null)
                            {
                                if (!hasUnresolvedArg)
                                {
                                    firstUnresolvedExpression = valueExp;
                                    hasUnresolvedArg = true;
                                }
                            }
                            else
                            {
                                _localVars.Put(argName, tm);
                                resolvedAnArg = true;
                            }
                        }
                        catch (InvalidReferenceException e)
                        {
                            if (!hasUnresolvedArg)
                            {
                                hasUnresolvedArg = true;
                                firstReferenceException = e;
                            }
                        }
                    }
                } while (resolvedAnArg && hasUnresolvedArg);
                if (hasUnresolvedArg)
                {
                    if (firstReferenceException != null)
                    {
                        throw firstReferenceException;
                    }
                    AssertNonNull(null, firstUnresolvedExpression, env);
                }
            }

            /**
             * @return the local variable of the given name
             * or null if it doesn't exist.
             */

            public ITemplateModel GetLocalVariable(string name)
            {
                return _localVars.Get(name);
            }

            internal Environment.Namespace GetLocals()
            {
                return _localVars;
            }

            /**
             * Set a local variable in this macro 
             */
            internal void SetLocalVar(string name, ITemplateModel var)
            {
                _localVars.Put(name, var);
            }

            public IEnumerable<string> GetLocalVariableNames()
            {
                var result = new HashSet<string>();
                for (ITemplateModelIterator it = _localVars.Keys().Iterator(); it.HasNext();)
                {
                    result.Add(it.Next().ToString());
                }
                return result;
            }
        }
    }
}